Popular Searches
Popular Course Categories
Popular Courses

Understanding polymorphism and overriding methods

Understanding polymorphism and overriding methods

Object-Oriented Programming in Dart

Understanding Polymorphism and Method Overriding in Dart

Polymorphism and method overriding are important concepts of Object-Oriented Programming (OOP) in Dart. They allow different classes to provide different implementations of the same method while still being accessed through a common parent type.

These concepts are especially useful in Flutter applications because Flutter and Dart rely heavily on classes, inheritance, abstract classes, interfaces, and overridden methods. JustAcademy's Flutter curriculum includes Object-Oriented Programming in Dart, inheritance, polymorphism, and abstraction as part of its Dart Programming Fundamentals module. Explore JustAcademy's Flutter Training Course .


1. What is Polymorphism?

The word polymorphism comes from two Greek words:

  • Poly = Many
  • Morph = Forms

Therefore, polymorphism means "one interface or reference, many forms of behavior."

In Dart, polymorphism allows a parent class reference to refer to objects of different child classes. When an overridden method is called, Dart executes the implementation belonging to the actual object.

Simple Example

class Animal {
  void sound() {
    print("Animal makes a sound");
  }
}

class Dog extends Animal {
  @override
  void sound() {
    print("Dog barks");
  }
}

class Cat extends Animal {
  @override
  void sound() {
    print("Cat meows");
  }
}

void main() {
  Animal animal1 = Dog();
  Animal animal2 = Cat();

  animal1.sound();
  animal2.sound();
}

Output:

Dog barks
Cat meows

Here, Animal is the common parent type, while Dog and Cat provide different implementations of sound().


2. What is Method Overriding?

Method overriding occurs when a child class provides its own implementation of a method that is already defined in its parent class.

The child class keeps the same method purpose but changes its behavior according to its own requirements.

Basic Syntax

class Parent {
  void display() {
    print("Parent method");
  }
}

class Child extends Parent {
  @override
  void display() {
    print("Child method");
  }
}

The @override annotation tells Dart that the child class is intentionally overriding an inherited member.


3. Relationship Between Inheritance, Overriding, and Polymorphism

These three concepts are closely related but are not the same.

  • Inheritance: Allows a child class to reuse and extend a parent class.
  • Method overriding: Allows a child class to provide a new implementation of an inherited method.
  • Polymorphism: Allows a common parent type to work with objects having different implementations.

The relationship can be represented as:

Parent Class
     |
     | extends
     v
Child Class
     |
     | overrides method
     v
Different Behavior
     |
     v
Polymorphism

4. Why Do We Use Polymorphism?

Polymorphism helps developers create flexible and reusable applications.

Major Benefits

  • Reduces unnecessary conditional logic.
  • Improves code reusability.
  • Makes applications easier to extend.
  • Supports loose coupling.
  • Allows different objects to follow a common interface.
  • Makes large applications easier to maintain.
  • Works naturally with abstract classes and interfaces.
  • Is useful for Flutter widgets, services, repositories, models, and application architecture.

5. Understanding the @override Annotation

Dart provides the @override annotation to indicate that a method, getter, or setter is overriding an inherited member.

class Animal {
  void eat() {
    print("Animal is eating");
  }
}

class Dog extends Animal {
  @override
  void eat() {
    print("Dog is eating");
  }
}

Using @override is recommended because it makes the programmer's intention clear and helps catch mistakes when the inherited member does not match what is being overridden.


6. Basic Method Overriding Example

class Vehicle {
  void start() {
    print("Vehicle is starting");
  }
}

class Car extends Vehicle {
  @override
  void start() {
    print("Car starts with a key");
  }
}

void main() {
  Car car = Car();
  car.start();
}

Output:

Car starts with a key

The Car class inherits from Vehicle, but it provides its own implementation of start().


7. Calling the Parent Method Using super

Sometimes a child class needs to extend the behavior of the parent method instead of completely replacing it. Dart provides super for this purpose.

class Animal {
  void sound() {
    print("Animal makes a sound");
  }
}

class Dog extends Animal {
  @override
  void sound() {
    super.sound();
    print("Dog barks");
  }
}

void main() {
  Dog dog = Dog();
  dog.sound();
}

Output:

Animal makes a sound
Dog barks

Here, super.sound() calls the implementation defined in the parent class.


8. Parent Reference and Child Object

One of the most important parts of polymorphism is that a variable declared using a parent type can hold an object created from a child class.

class Animal {
  void sound() {
    print("Animal sound");
  }
}

class Dog extends Animal {
  @override
  void sound() {
    print("Dog barks");
  }
}

void main() {
  Animal animal = Dog();

  animal.sound();
}

Output:

Dog barks

Although the variable is declared as Animal, the actual object is a Dog. Therefore, the overridden Dog.sound() implementation is executed.


9. Multiple Child Classes with Polymorphism

Multiple child classes can override the same parent method in different ways.

class Payment {
  void pay() {
    print("Making payment");
  }
}

class CreditCardPayment extends Payment {
  @override
  void pay() {
    print("Payment made using Credit Card");
  }
}

class UpiPayment extends Payment {
  @override
  void pay() {
    print("Payment made using UPI");
  }
}

class CashPayment extends Payment {
  @override
  void pay() {
    print("Payment made using Cash");
  }
}

void main() {
  Payment payment1 = CreditCardPayment();
  Payment payment2 = UpiPayment();
  Payment payment3 = CashPayment();

  payment1.pay();
  payment2.pay();
  payment3.pay();
}

Output:

Payment made using Credit Card
Payment made using UPI
Payment made using Cash

The same pay() operation behaves differently depending on the actual object.


10. Polymorphism with a List

Polymorphism becomes especially useful when multiple child objects need to be processed through a common parent type.

class Animal {
  void sound() {
    print("Animal sound");
  }
}

class Dog extends Animal {
  @override
  void sound() {
    print("Dog barks");
  }
}

class Cat extends Animal {
  @override
  void sound() {
    print("Cat meows");
  }
}

class Cow extends Animal {
  @override
  void sound() {
    print("Cow moos");
  }
}

void main() {
  List animals = [
    Dog(),
    Cat(),
    Cow()
  ];

  for (Animal animal in animals) {
    animal.sound();
  }
}

Output:

Dog barks
Cat meows
Cow moos

This approach is useful because the loop does not need to know whether the object is a dog, cat, or cow. It only needs to know that every object is an Animal.


11. Practical Example: Employee and Developer

class Employee {
  String name;

  Employee(this.name);

  void work() {
    print("$name is working");
  }
}

class Developer extends Employee {
  Developer(String name) : super(name);

  @override
  void work() {
    print("$name is writing code");
  }
}

class Designer extends Employee {
  Designer(String name) : super(name);

  @override
  void work() {
    print("$name is designing UI");
  }
}

void main() {
  List employees = [
    Developer("Rahul"),
    Designer("Priya")
  ];

  for (Employee employee in employees) {
    employee.work();
  }
}

Output:

Rahul is writing code
Priya is designing UI

The application can treat developers and designers as employees while allowing each class to perform its own version of work().


12. Practical Example: Notification System

class Notification {
  void send() {
    print("Sending notification");
  }
}

class EmailNotification extends Notification {
  @override
  void send() {
    print("Sending notification through Email");
  }
}

class SmsNotification extends Notification {
  @override
  void send() {
    print("Sending notification through SMS");
  }
}

class PushNotification extends Notification {
  @override
  void send() {
    print("Sending Push Notification");
  }
}

void processNotification(Notification notification) {
  notification.send();
}

void main() {
  processNotification(EmailNotification());
  processNotification(SmsNotification());
  processNotification(PushNotification());
}

The function processNotification() accepts the common parent type Notification. It does not need separate functions for every notification type.


13. Practical Example: E-Commerce Product

class Product {
  void calculatePrice() {
    print("Calculating product price");
  }
}

class ElectronicsProduct extends Product {
  @override
  void calculatePrice() {
    print("Calculating electronics product price");
  }
}

class ClothingProduct extends Product {
  @override
  void calculatePrice() {
    print("Calculating clothing product price");
  }
}

class GroceryProduct extends Product {
  @override
  void calculatePrice() {
    print("Calculating grocery product price");
  }
}

void main() {
  List products = [
    ElectronicsProduct(),
    ClothingProduct(),
    GroceryProduct()
  ];

  for (Product product in products) {
    product.calculatePrice();
  }
}

This structure can be extended later by adding more product classes without changing the common processing code.


14. Practical Example: Shape and Area

Polymorphism is commonly used when different objects have a common operation but require different calculations.

abstract class Shape {
  double calculateArea();
}

class Circle extends Shape {
  double radius;

  Circle(this.radius);

  @override
  double calculateArea() {
    return 3.14 * radius * radius;
  }
}

class Rectangle extends Shape {
  double width;
  double height;

  Rectangle(this.width, this.height);

  @override
  double calculateArea() {
    return width * height;
  }
}

void main() {
  List shapes = [
    Circle(5),
    Rectangle(10, 4)
  ];

  for (Shape shape in shapes) {
    print(shape.calculateArea());
  }
}

Both Circle and Rectangle implement calculateArea(), but the calculation is different for each shape.


15. Polymorphism with Abstract Classes

Abstract classes are frequently used as a common contract for polymorphic objects.

abstract class Payment {
  void makePayment();
}

class UpiPayment extends Payment {
  @override
  void makePayment() {
    print("Payment made through UPI");
  }
}

class CardPayment extends Payment {
  @override
  void makePayment() {
    print("Payment made through Card");
  }
}

void processPayment(Payment payment) {
  payment.makePayment();
}

void main() {
  processPayment(UpiPayment());
  processPayment(CardPayment());
}

The Payment class defines what a payment object must provide, while individual classes define how the operation is performed.


16. Overriding Getters

Dart also allows inherited getters to be overridden.

class Person {
  String get role {
    return "Person";
  }
}

class Teacher extends Person {
  @override
  String get role {
    return "Teacher";
  }
}

void main() {
  Person person = Teacher();

  print(person.role);
}

Output:

Teacher

17. Overriding Setters

Setters can also be overridden when a child class needs different behavior.

class Account {
  double balance = 0;

  set amount(double value) {
    balance = value;
  }
}

class SavingsAccount extends Account {
  @override
  set amount(double value) {
    if (value >= 0) {
      balance = value;
    }
  }
}

The child class can introduce additional validation while retaining the same conceptual property.


18. Polymorphic Functions

A function can accept a parent type and work with many different child objects.

class Animal {
  void sound() {
    print("Animal sound");
  }
}

class Dog extends Animal {
  @override
  void sound() {
    print("Dog barks");
  }
}

class Cat extends Animal {
  @override
  void sound() {
    print("Cat meows");
  }
}

void makeAnimalSound(Animal animal) {
  animal.sound();
}

void main() {
  makeAnimalSound(Dog());
  makeAnimalSound(Cat());
}

This is a clean example of polymorphism because the function does not need to know the exact child class.


19. Static Type vs Runtime Object

Understanding the difference between the declared type and the actual object is important for understanding polymorphism.

Animal animal = Dog();
  • Static/declared type: Animal
  • Runtime object: Dog

The variable can use members available through the Animal type, while overridden behavior can come from the actual Dog object.


20. Method Overriding vs Method Overloading

Feature Method Overriding Method Overloading
Purpose Change inherited behavior Provide different ways to call an operation
Classes Usually involves parent and child classes Does not inherently require inheritance
Method name Same inherited member Usually the same name with different parameter signatures
Dart support Supported Traditional same-name parameter-list overloading is not supported
Common Dart alternative @override Optional/named parameters or separate method names

Example of Dart's Optional Parameters Instead of Traditional Overloading

class Calculator {
  int add(int a, int b, [int c = 0]) {
    return a + b + c;
  }
}

void main() {
  print(Calculator().add(10, 20));
  print(Calculator().add(10, 20, 30));
}

21. Method Overriding vs Inheritance

Concept Meaning
Inheritance Allows a child class to inherit and extend a parent class.
Overriding Allows the child to replace or specialize inherited behavior.
Polymorphism Allows common code to work with different object implementations.

22. Flutter Example of Method Overriding

Flutter uses inheritance and method overriding extensively. For example, custom widgets commonly extend Flutter widget classes and implement the required behavior.

import 'package:flutter/material.dart';

class WelcomeScreen extends StatelessWidget {
  const WelcomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Welcome"),
      ),
      body: const Center(
        child: Text("Hello Flutter"),
      ),
    );
  }
}

Here, WelcomeScreen extends StatelessWidget and overrides the build() method to define the widget's UI.


23. Flutter Example with Polymorphism

abstract class AppButton {
  Widget buildButton();
}

class LoginButton extends AppButton {
  @override
  Widget buildButton() {
    return const Text("Login");
  }
}

class LogoutButton extends AppButton {
  @override
  Widget buildButton() {
    return const Text("Logout");
  }
}

Both button classes follow the same AppButton contract while providing different implementations.


24. Practical Delivery System Example

abstract class Delivery {
  void deliver();
}

class BikeDelivery extends Delivery {
  @override
  void deliver() {
    print("Delivering order by bike");
  }
}

class TruckDelivery extends Delivery {
  @override
  void deliver() {
    print("Delivering order by truck");
  }
}

class DroneDelivery extends Delivery {
  @override
  void deliver() {
    print("Delivering order by drone");
  }
}

void processDelivery(Delivery delivery) {
  delivery.deliver();
}

void main() {
  processDelivery(BikeDelivery());
  processDelivery(TruckDelivery());
  processDelivery(DroneDelivery());
}

This design allows the delivery system to support new delivery methods without changing the processDelivery() function.


25. Practical Login/User Example

class User {
  void accessDashboard() {
    print("User dashboard");
  }
}

class Admin extends User {
  @override
  void accessDashboard() {
    print("Admin dashboard with management controls");
  }
}

class Customer extends User {
  @override
  void accessDashboard() {
    print("Customer dashboard with shopping options");
  }
}

void openDashboard(User user) {
  user.accessDashboard();
}

void main() {
  openDashboard(Admin());
  openDashboard(Customer());
}

The dashboard operation changes according to the actual user type.


26. A Complete Polymorphism Example

abstract class Employee {
  String name;

  Employee(this.name);

  void calculateSalary();
}

class Developer extends Employee {
  Developer(String name) : super(name);

  @override
  void calculateSalary() {
    print("$name receives developer salary");
  }
}

class Designer extends Employee {
  Designer(String name) : super(name);

  @override
  void calculateSalary() {
    print("$name receives designer salary");
  }
}

class Manager extends Employee {
  Manager(String name) : super(name);

  @override
  void calculateSalary() {
    print("$name receives manager salary");
  }
}

void processSalary(Employee employee) {
  employee.calculateSalary();
}

void main() {
  List employees = [
    Developer("Amit"),
    Designer("Neha"),
    Manager("Rohit")
  ];

  for (Employee employee in employees) {
    processSalary(employee);
  }
}

This example demonstrates inheritance, abstraction, method overriding, and polymorphism together.


27. Advantages of Method Overriding

  • Allows specialized child-class behavior.
  • Supports runtime polymorphic behavior.
  • Improves flexibility of application design.
  • Promotes code reuse through inheritance.
  • Allows common parent-level APIs.
  • Makes code easier to extend.
  • Works well with abstract classes and interfaces.
  • Is useful for scalable Flutter application architecture.

28. Advantages of Polymorphism

  • Flexibility: The same operation can work with different object types.
  • Reusability: Common processing logic can be written once.
  • Maintainability: Changes can often be isolated to individual implementations.
  • Extensibility: New child classes can be added without rewriting common logic.
  • Loose coupling: Code can depend on an abstraction instead of a concrete implementation.
  • Cleaner architecture: Different responsibilities can be represented by different classes.

29. Disadvantages and Limitations

  • Excessive inheritance can make class relationships difficult to understand.
  • Deep inheritance hierarchies can become difficult to maintain.
  • Overriding too many methods can make behavior harder to trace.
  • Incorrect use of inheritance can create unnecessary coupling.
  • Developers must understand the parent contract before overriding its members.

30. Important Rules for Method Overriding in Dart

  1. The child class must inherit from or otherwise implement the relevant parent contract.
  2. The overridden member should be compatible with the inherited declaration.
  3. Use @override to clearly indicate intentional overriding.
  4. Do not arbitrarily change the meaning of a method in a way that violates the parent contract.
  5. Use super when the parent implementation also needs to run.
  6. Keep overridden methods focused on the responsibility represented by the parent API.

31. Common Mistakes

Mistake 1: Forgetting @override

Although Dart can override inherited members without requiring the annotation in every situation, using @override makes the intent explicit and helps catch errors.

Mistake 2: Incorrect Method Signature

class Parent {
  void display(String name) {
    print(name);
  }
}

When overriding an inherited method, the child implementation must be compatible with the inherited member rather than arbitrarily changing its contract.

Mistake 3: Confusing Overriding with Overloading

Overriding changes inherited behavior. Traditional same-name method overloading based only on different parameter lists is not Dart's normal method model.

Mistake 4: Overusing Inheritance

Not every "is-a" or code-reuse problem should automatically be solved with inheritance. Composition can sometimes provide a cleaner design.


32. Inheritance vs Composition

Inheritance represents an is-a relationship, while composition generally represents a has-a relationship.

Inheritance Example

class Vehicle {
}

class Car extends Vehicle {
}

A car is a vehicle.

Composition Example

class Engine {
  void start() {
    print("Engine started");
  }
}

class Car {
  final Engine engine;

  Car(this.engine);
}

A car has an engine.

Choosing between inheritance and composition depends on the actual relationship and design requirements.


33. Best Practices

  • Use @override whenever intentionally overriding an inherited member.
  • Use meaningful parent abstractions.
  • Keep parent classes focused.
  • Do not create inheritance relationships only to reuse a small amount of code.
  • Use abstract classes when defining common contracts is appropriate.
  • Use polymorphic functions to reduce repetitive type-specific logic.
  • Prefer composition when the relationship is "has-a" rather than "is-a".
  • Keep overridden methods consistent with the expectations of the parent abstraction.
  • Use super when parent behavior should be preserved and extended.

34. Quick Revision Table

Concept Description Example
Inheritance Child class inherits from parent class class Dog extends Animal
Overriding Child provides its own implementation @override void sound()
Polymorphism One parent type works with different child objects Animal animal = Dog();
super Accesses parent implementation super.sound()
Abstract class Provides a common abstraction/contract abstract class Payment
@override Marks an inherited member as intentionally overridden @override

35. Practice Exercises

  1. Create an Animal parent class with a sound() method.
  2. Create Dog, Cat, and Cow child classes.
  3. Override sound() in every child class.
  4. Create a Payment abstract class and implement UPI, Card, and Cash payments.
  5. Create an Employee parent class and Developer, Designer, and Manager child classes.
  6. Store different employee objects in a List.
  7. Create a Shape abstraction with Circle and Rectangle implementations.
  8. Create a Flutter widget that extends StatelessWidget and overrides build().
  9. Create a notification system using Email, SMS, and Push notification classes.
  10. Create a delivery system using Bike, Truck, and Drone delivery classes.

36. Key Takeaways

  • Polymorphism means one common interface or parent type can represent different forms of behavior.
  • Method overriding allows a child class to provide its own implementation of inherited behavior.
  • @override is used to clearly indicate an overridden member.
  • super can be used to call the parent implementation.
  • A parent-type variable can refer to a child object.
  • Different child objects can respond differently to the same method call.
  • Lists containing a parent type can contain objects of multiple child classes.
  • Abstract classes are useful for creating common contracts for polymorphic implementations.
  • Polymorphism helps make applications flexible, reusable, and easier to extend.
  • Flutter development uses inheritance and overriding extensively, including widget classes and their methods.

37. JustAcademy Flutter Resources

JustAcademy's Flutter Training curriculum includes Dart OOP concepts such as classes, objects, constructors, inheritance, polymorphism, and abstraction. :contentReference[oaicite:0]{index=0}

Flutter Training Course: https://www.justacademy.co/course-detail/flutter-training

Register for Course Demo: https://www.justacademy.co/register-for-course-demo

Previous Next
whatsapp